#!/usr/bin/env python
'''Branch specific file locks

This extension provides advisory branch specific locks. The locks are propagated in a special .lock file in the root of the repository. This file is versioned like any other file and thus travels with pushes and pulls.

NOTE that you must specifically commit and push the lock file to a central repo for it to become visible for others.
'''

import os, stat, sys
from mercurial import hg, ui

class Lock:
    def lockfile(self):
        return os.path.join(self.repo.root, ".locks")

    def absPath(self, path):
        return os.path.join(self.repo.root, self.relativePath(path))

    def relativePath(self, path):
        if not path.startswith(self.repo.root):
            path = os.path.join(os.getcwd(), path) 
        return path[len(self.repo.root) + 1:]


    def __init__(self, ui, repo):
        self.ui = ui
        self.repo = repo

    def lock(self, path):
        if not os.path.exists(path):
            return
        perms = os.stat(path).st_mode
        os.chmod(path, perms & ~stat.S_IWRITE)

    def user(self):
        user = self.ui.config("ui", "username")
        if user == None:
            raise "No username specified -- needed by locking module"
        return user

    def unlock(self, path):
        if not os.path.exists(path):
            self.ui.warn("Path to be unlocked does not exists '%s'\n" % path)
            return
        perms = os.stat(path).st_mode
        os.chmod(path, perms | stat.S_IWRITE)

    @classmethod
    def parseLocklines(self, data):
        for line in data.splitlines():
            path, locker = line.split("=")
            yield [path.strip(), locker.strip()]

    def lockers(self):
        if not os.path.isfile(self.lockfile()):
            return
        fh = open(self.lockfile(), "r")
        try:
            for p in self.parseLocklines(fh.read()):
                yield p
        finally:
            fh.close()

    def lockedFiles(self):
        for path, locker in self.lockers():
            if locker != self.user():
                yield path

    def lockLine(self, path, user):
        return path + "=" + user

    def releaseLock(self, releasePath):
        self.unlock(self.absPath(releasePath))
        locks = []
        relativePath = self.relativePath(releasePath)
        for path, locker in self.lockers():
            if path != relativePath:
                locks.append(self.lockLine(path, locker))
        fh = open(self.lockfile(), "w+")
        try:
            for lock in locks:
                fh.write("%s\n" % lock)
        finally:
            fh.close()

    def getLock(self, getPath):
        relativePath = self.relativePath(getPath)
        if not os.path.exists(self.absPath(getPath)):
            raise AssertionError("Path '%s' does not exist" % self.absPath(getPath))

        for path, locker in self.lockers():
            if relativePath == path and locker != self.user():
                self.ui.warn("%s is already locked by %s\n" % (path, locker))
                sys.exit(1)
            if relativePath == path:
                self.ui.warn("you own the lock already for %s\n" % path)
                return

        fh = open(self.lockfile(), "a+")
        try:
            fh.write("%s\n" % self.lockLine(relativePath, self.user()))
        finally:
            fh.close()

def lockfileDataInvariant(locklines):
    paths = {}
    for path, locker in locklines:
        if path in paths:
            yield path
        else:
            paths[path] = True

def unlockAll(ui, repo, **kws):
    lock = Lock(ui, repo)
    for path in lock.lockedFiles():
        lock.unlock(path)
    return False
    
def lockAll(ui, repo, **kws):
    lock = Lock(ui, repo)
    for path, user in lock.lockers():
        if user == lock.user():
            lock.unlock(path)
        else:
            lock.lock(path)
    return False

def getLock(ui, repo, *filenames):
    """Lock the given files using your username."""
    for filename in filenames:
        Lock(ui, repo).getLock(filename)

def releaseLock(ui, repo, *filenames):
    """Release the lock for files (even if you do not own the locks)."""
    for filename in filenames:
        print "releasing %s" % filename
        Lock(ui, repo).releaseLock(filename)

def refreshLocks(ui, repo):
    """Read lock file and apply the locks to repository files. This could be needed after a merge conflict in the .lock file.
    """
    lockAll(ui, repo)

def checkWorkingLockfile(ui, repo, **kws):
    """Hook to check that the working lockfile is valid."""
    lock = Lock(ui, repo)
    if not os.path.isfile(lock.lockfile()):
        return False
    fh = open(lock.lockfile(), "r")
    try:
        failures = list(lockfileDataInvariant(Lock.parseLocklines(fh.read())))
        if failures:
            ui.warn("Multiple locks for files %s!\nEdit '%s' and remove the incorrect lines before committing.\n" 
                    % (failures, lock.lockfile()))
            return True
    finally:
        fh.close()
        
    
def verifyLockfileHook(node, repo, ui, **kws):
    """hook to verify that the lockfile is valid."""
    ctx = repo[node]
    lock = Lock(ui, repo)
    if lock.lockfile() in ctx: 
        locks = ctx[lock.lockfile()].data()
        failures = list(lockfileDataInvariant(Lock.parseLocklines(locks)))
        if failures:
            ui.warn("Multiple locks for files %s!\nEdit '%s' and remove the incorrect lines.\n" 
                    % (failures, lock.lockfile()))
            return True
    return False

def uisetup(ui):
    """Locking extensions needs hooks around update."""
    if ui.config("ui","username") == None:
        raise "You must setup ui.username for the filelock module to work."
    ui.setconfig("hooks", "preupdate.unlock", unlockAll)

def reposetup(ui, repo):
    if ui.config("ui","username") == None:
        raise "You must setup ui.username for the filelock module to work."
    ui.setconfig("hooks", "update.lock", lockAll)
    ui.setconfig("hooks", "update.checkLockfile", checkWorkingLockfile)
    ui.setconfig("hooks", "pretxncommit.verifyLockfile", verifyLockfileHook)

cmdtable = {
    "lock" : (getLock, [], "hg lock <filenames>"),
    "unlock" : (releaseLock, [], "hg unlock <filenames>"),
    "refresh-locks" : (refreshLocks, [], "hg refresh-locks")
    }

